#include "IsoBandPolygons.h"
#include <iomanip>
#include <iostream>

vector<IsoBandPoint> skip_coordinate(double x, double y, int mode)
{
    if (mode == 0)
    {
        /* down */
        x++;
    }
    else if (mode == 1)
    {
        /* left */
        /* do nothing */
    }
    else if (mode == 2)
    {
        /* up */
        y++;
    }
    else if (mode == 3)
    {
        /* right */
        x++;
        y++;
    }

    return {{x, y}};
}

vector<IsoBandPoint> entry_coordinate(double x, double y, int mode, const vector<double> &path)
{
    if (mode == 0)
    {
        /* down */
        x += 1;
        y += path[1];
    }
    else if (mode == 1)
    {
        /* left */
        x += path[0];
    }
    else if (mode == 2)
    {
        /* up */
        y += path[1];
    }
    else if (mode == 3)
    {
        /* right */
        x += path[0];
        y += 1;
    }
    return {{x, y}};
}

vector<IsoBandPolygon> traceBandPaths(std::vector<double> &gridData, int width, int height,
                                      vector<vector<Cell>> &cellGrid,
                                      double minV, double maxV)

{
    vector<IsoBandPolygon> polygons;

    int height1 = height - 1;
    int width1 = width - 1;

    /*
     * directions for out-of-grid moves are:
     * 0 ... "down",
     * 1 ... "left",
     * 2 ... "up",
     * 3 ... "right"
     */
    static vector<vector<string>> valid_entries = {
        {"rt", "rb"}, /* down */
        {"br", "bl"}, /* left */
        {"lb", "lt"}, /* up */
        {"tl", "tr"}  /* right */
    };
    static vector<int> add_x = {0, -1, 0, 1};
    static vector<int> add_y = {-1, 0, 1, 0};
    static vector<string> available_starts = {"bl", "lb", "lt", "tl", "tr", "rt", "rb", "br"};
    static unordered_map<string, int> entry_dir = {
        {"bl", 1}, {"br", 1}, {"lb", 2}, {"lt", 2}, {"tl", 3}, {"tr", 3}, {"rt", 0}, {"rb", 0}};

    if (requireFrame(gridData, width, height, minV, maxV))
    {
        IsoBandPolygon p;
        p.coords = {{0, 0}, {0, height1}, {width1, height1}, {width1, 0}, {0, 0}};

        double outArea;
        std::vector<double> outBbox;
        ringArea(p.coords, outArea, outBbox);
        p.area = outArea;
        p.bbox = outBbox;
        polygons.push_back(std::move(p));
    }

    /* finally, start tracing back first polygon(s) */
    for (int i = 0; i < width1; i++)
    {
        for (int j = 0; j < height1; j++)
        {
            string nextedge;

            /* trace paths for all available edges that go through this cell */
            for (int e = 0; e < 8; e++)
            {
                nextedge = available_starts[e];

                auto &cell = cellGrid[i][j];

                if (!cell.edges.isObject(nextedge))
                    continue;

                auto ee = cell.edges.find(nextedge);

                /* start a new, full path */
                vector<IsoBandPoint> path;
                string enter = nextedge;
                int x = i;
                int y = j;
                bool finalized = false;
                IsoBandPoint origin = {i + ee.path[0], j + ee.path[1]};

                /* add start coordinate */
                path.push_back(origin);

                /* start traceback */
                while (!finalized && x > -1 && y > -1 && x < cellGrid.size() && y < cellGrid[0].size())
                {
                    Cell &cc = cellGrid[x][y];
                    if (!cc.edges.isObject(enter))
                        break;

                    auto ee = cc.edges.find(enter);
                    cc.edges.remove(enter);

                    /* add last point of edge to path array, since we extend a polygon */
                    IsoBandPoint point = {ee.path[2], ee.path[3]};
                    point.x += x;
                    point.y += y;
                    path.push_back(point);

                    enter = ee.enter;
                    x = x + ee.moveX;
                    y = y + ee.moveY;

                    /* handle out-of-grid moves */
                    if (x < 0 || x >= cellGrid.size() || y < 0 || y >= cellGrid[x].size())
                    {
                        int dir = 0;
                        int count = 0;

                        if (x == width1)
                        {
                            x--;
                            dir = 0; /* move downwards */
                        }
                        else if (x < 0)
                        {
                            x++;
                            dir = 2; /* move upwards */
                        }
                        else if (y == height1)
                        {
                            y--;
                            dir = 3; /* move right */
                        }
                        else if (y < 0)
                        {
                            y++;
                            dir = 1; /* move left */
                        }
                        else
                        {
                            throw runtime_error("Left the grid somewhere in the interior!");
                        }

                        if (x == i && y == j && dir == entry_dir[nextedge])
                        {
                            finalized = true;
                            enter = nextedge;
                            break;
                        }

                        while (true)
                        {
                            bool found_entry = false;

                            if (count > 4)
                                throw runtime_error("Direction change counter overflow! This should never happen!");

                            if (x >= 0 && x < cellGrid.size() && y >= 0 && y < cellGrid[x].size())
                            {
                                Cell &cc = cellGrid[x][y];

                                /* check for re-entry */
                                for (int s = 0; s < valid_entries[dir].size(); s++)
                                {
                                    string ve = valid_entries[dir][s];
                                    if (cc.edges.isObject(ve))
                                    {
                                        auto ee = cc.edges.find(ve);
                                        /* found re-entry */
                                        vector<IsoBandPoint> entry = entry_coordinate(x, y, dir, ee.path);
                                        path.insert(path.end(), entry.begin(), entry.end());

                                        enter = ve;
                                        found_entry = true;
                                        break;
                                    }
                                }
                            }

                            if (found_entry)
                            {
                                break;
                            }
                            else
                            {
                                vector<IsoBandPoint> skip = skip_coordinate(x, y, dir);
                                path.insert(path.end(), skip.begin(), skip.end());

                                x += add_x[dir];
                                y += add_y[dir];

                                /* change direction if we've moved out of grid again */
                                if (x < 0 || x >= cellGrid.size() || y < 0 || y >= cellGrid[x].size())
                                {
                                    if ((dir == 0 && y < 0) || (dir == 1 && x < 0) ||
                                        (dir == 2 && y == height1) || (dir == 3 && x == width1))
                                    {
                                        x -= add_x[dir];
                                        y -= add_y[dir];

                                        dir = (dir + 1) % 4;
                                        count++;
                                    }
                                }

                                if (x == i && y == j && dir == entry_dir[nextedge])
                                {
                                    /* we are back where we started off, so finalize the polygon */
                                    finalized = true;
                                    enter = nextedge;
                                    break;
                                }
                            }
                        }
                    }
                }

                if (path[path.size() - 1].x != origin.x || path[path.size() - 1].y != origin.y)
                    path.push_back(origin);

                IsoBandPolygon p;
                p.coords = std::move(path);

                double outArea;
                std::vector<double> outBbox;
                ringArea(p.coords, outArea, outBbox);
                p.area = outArea;
                p.bbox = outBbox;
                polygons.push_back(std::move(p));

            } /* end forall entry sites */
        } /* end foreach i */
    } /* end foreach j */

    return polygons;
}

inline bool requireLineFrame(std::vector<double> &gridData, int width, int height, double threshold)
{
    int cols, rows, i, j;

    bool frameRequired = true;

    for (j = 0; j < rows; j++)
    {
        if (gridData[j * width + 0] >= threshold || gridData[j * width + cols - 1] >= threshold)
        {
            frameRequired = false;
            break;
        }
    }

    if (frameRequired && (gridData[(rows - 1) * width + 0] >= threshold || gridData[(rows - 1) * width + (cols - 1)] >= threshold))
    {
        frameRequired = false;
    }

    if (frameRequired)
    {
        for (i = 0; i < cols - 1; i++)
        {
            if (gridData[i] >= threshold || gridData[(rows - 1) * width + i] > threshold)
            {
                frameRequired = false;
                break;
            }
        }
    }

    return frameRequired;
}

IsoBandPoint entry_coordinate2(int x, int y, int mode, const vector<double> &path)
{
    if (mode == 0)
    {
        /* down */
        x += 1;
        y += path[1];
    }
    else if (mode == 1)
    {
        /* left */
        x += path[0];
    }
    else if (mode == 2)
    {
        /* up */
        y += path[1];
    }
    else if (mode == 3)
    {
        /* right */
        x += path[0];
        y += 1;
    }

    return {x, y};
}

IsoBandPoint skip_coordinate2(int x, int y, int mode)
{
    if (mode == 0)
    {
        /* down */
        x++;
    }
    else if (mode == 1)
    {
        // nothing
    }
    else if (mode == 2)
    {
        /* up */
        y++;
    }
    else if (mode == 3)
    {
        /* right */
        x++;
        y++;
    }

    return {x, y};
}

vector<IsoBandPolygon> traceLinePaths(std::vector<double> &gridData, int width, int height,
                                      vector<vector<Cell>> &cellGrid, double threshold, bool linearRing, bool noFrame)
{
    vector<IsoBandPolygon> polygons;

    int height1 = height - 1;
    int width1 = width - 1;

    static vector<string> valid_entries = {
        "right", "bottom", "left", "top"};
    static vector<int> add_x = {0, -1, 0, 1};
    static vector<int> add_y = {-1, 0, 1, 0};
    static unordered_map<string, int> entry_dir = {
        {"bottom", 1}, {"left", 2}, {"top", 3}, {"right", 0}};

    if (!noFrame)
    {
        if (requireLineFrame(gridData, width1, height1, threshold))
        {
            if (linearRing)
            {
                IsoBandPolygon p;
                p.coords = {{0, 0}, {0, height1}, {width1, height1}, {width1, 0}, {0, 0}};
                polygons.push_back(std::move(p));
            }
            else
            {
                IsoBandPolygon p;
                p.coords = {{0, 0}, {0, height1}, {width1, height1}, {width1, 0}};
                polygons.push_back(std::move(p));
            }
        }
    }

    EdgesInfo ee;
    std::string enter;
    std::string ve;
    int x, y;
    bool found_entry;
    int dir, count;

    int deubgger = 0;
    bool finalized;

    for (auto i = 0; i < width1; i++)
    {
        for (auto j = 0; j < height1; j++)
        {
            auto cell = cellGrid[i][j];
            std::string nextedge;
            for (auto e = 0; e < 4; e++)
            {
                nextedge = valid_entries[e];
                if (!cell.edges.isObject(nextedge))
                    continue;
               
                std::vector<IsoBandPoint> path;
               
                ee = cell.edges.find(nextedge);
                enter = nextedge;
                x = i;
                y = j;
                finalized = false;
                IsoBandPoint origin = {i + ee.path[0], j + ee.path[1]};
                path.push_back(origin);
                // std::cout << "x:" << origin.x << "\t";
                // std::cout << "i:" << i << "\t";
                // std::cout << "ee.path[0]:" << ee.path[0] << "\n";
                while (!finalized)
                {
                    auto& cc = cellGrid[x][y];
                    if (!cc.edges.isObject(enter))
                        break;
                    ee = cc.edges.find(enter);
                    cc.edges.remove(enter);

                    IsoBandPoint point = {ee.path[2], ee.path[3]};
                    point.x += x;
                    point.y += y;
                    path.push_back(point);

                    enter = ee.enter;
                    x = x + ee.moveX;
                    y = y + ee.moveY;

                    if (x < 0 || x >= cellGrid.size() || y < 0 || y >= cellGrid[x].size())
                    {
                        if (!linearRing)
                            break;
                        dir = 0;
                        count = 0;

                        if (x == width1)
                        {
                            x--;
                            dir = 0; /* move downwards */
                        }
                        else if (x < 0)
                        {
                            x++;
                            dir = 2; /* move upwards */
                        }
                        else if (y == height1)
                        {
                            y--;
                            dir = 3; /* move right */
                        }
                        else if (y < 0)
                        {
                            y++;
                            dir = 1; /* move left */
                        }

                        if (x == i && y == j && dir == entry_dir[nextedge])
                        {
                            finalized = true;
                            enter = nextedge;
                            break;
                        }

                        while (true)
                        {
                            found_entry = false;
                            if (x < 0 || x >= cellGrid.size() || y < 0 || y >= cellGrid[x].size())
                            {
                                cc = cellGrid[x][y];
                                /* check for re-entry */
                                ve = valid_entries[dir];
                                if (cc.edges.isObject(ve))
                                {
                                    /* found re-entry */
                                    ee = cc.edges.find(ve);
                                    path.push_back(entry_coordinate2(x, y, dir, ee.path));
                                    enter = ve;
                                    found_entry = true;
                                    break;
                                }
                            }
                            if (found_entry)
                            {
                                break;
                            }
                            else
                            {
                                path.push_back(skip_coordinate2(x, y, dir));
                                x += add_x[dir];
                                y += add_y[dir];

                                /* change direction if we'e moved out of grid again */
                                if (x > -1 && x < height && y > -1 && y < width && cellGrid[x][y].cval > -1)
                                {
                                    if (
                                        (dir == 0 && y < 0) ||
                                        (dir == 1 && x < 0) ||
                                        (dir == 2 && y == width1) ||
                                        (dir == 3 && x == height1))
                                    {
                                        x -= add_x[dir];
                                        y -= add_y[dir];

                                        dir = (dir + 1) % 4;
                                        count++;
                                    }
                                }

                                if (x == i && y == j && dir == entry_dir[nextedge])
                                {
                                    /* we are back where we started off, so finalize the polygon */
                                    finalized = true;
                                    enter = nextedge;
                                    break;
                                }
                            }
                        }
                    }
                
                }

                if (
                    linearRing &&
                    (path[path.size() - 1].x != origin.x || path[path.size() - 1].y != origin.y))
                {
                    path.push_back(origin);
                }

                IsoBandPolygon p;
                p.coords = std::move(path);

                polygons.push_back(p);
            }
        }
    }
    return polygons;
}
bool requireFrame(std::vector<double> &gridData, int width, int height, double lowerBound, double upperBound)
{
    bool frameRequired = true;

    for (int j = 0; j < height; j++)
    {
        if (gridData[j * width + 0] < lowerBound || gridData[j * width + 0] > upperBound ||
            gridData[j * width + width - 1] < lowerBound || gridData[j * width + width - 1] > upperBound)
        {
            frameRequired = false;
            break;
        }
    }

    if (frameRequired &&
        (gridData[(height - 1) * width + 0] < lowerBound || gridData[(height - 1) * width + 0] > upperBound ||
         gridData[(height - 1) * width + width - 1] < lowerBound || gridData[(height - 1) * width + width - 1] > upperBound))
    {
        frameRequired = false;
    }

    if (frameRequired)
        for (int i = 0; i < width - 1; i++)
        {
            if (gridData[0 * width + i] < lowerBound || gridData[0 * width + i] > upperBound ||
                gridData[(height - 1) * width + i] < lowerBound || gridData[(height - 1) * width + i] > upperBound)
            {
                frameRequired = false;
                break;
            }
        }

    return frameRequired;
}
